package tau.adx.common.contracts;

import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.SortedMap;
import java.util.TreeMap;

import lombok.Data;
import tau.adx.common.adn.AdNetwork;
import tau.adx.common.advertisers.Advertiser;
import tau.adx.common.enteties.ranking.RankingAgency;

import com.google.inject.Inject;

/**
 * @author greenwald
 * 
 */
public class ContractAssigner {

	/**
	 * Main {@link RankingAgency} in the system.
	 */
	RankingAgency rankingAgency;

	/**
	 * @param rankingAgency
	 *            Main {@link RankingAgency} in the system.
	 */
	@Inject
	public ContractAssigner(RankingAgency rankingAgency) {
		this.rankingAgency = rankingAgency;
	}

	/**
	 * Assigns {@link MonthlyContract monthly contracts} to {@link AdNetwork ad
	 * networks (agencies)}. Each {@link Advertiser} will be assigned to a
	 * single {@link AdNetwork}. The {@link Advertiser advertisers} are randomly
	 * split into two groups.<br>
	 * In one group, each {@link Advertiser} <i>d</i> randomly selects the
	 * budget B<i>d</i> for his {@link MonthlyContract}, and the contracts are
	 * allocated in decreasing budget (
	 * {@link MonthlyContract#getMonthlyBudget()}) order to {@link AdNetwork
	 * agencies} by decreasing quality rating.<br>
	 * The other group of contracts is auctioned among the agencies, where each
	 * agency bids for the contract budget.<br>
	 * This scheme rewards the highly rated Ad Networks by allocating them
	 * contracts of higher budgets (so they can proportionally increase their
	 * earnings), while giving all the Ad Network agennts an opportunity to
	 * execute marketing campaigns and improve their rating
	 * 
	 * @param advertisers
	 *            {@link Collection} of {@link Advertiser advertisers} to assign
	 *            {@link MonthlyContract monthly contracts} for.
	 * @param adNetworks
	 *            {@link Collection} of {@link AdNetwork ad networks} to assign
	 *            {@link MonthlyContract monthly contracts} to.
	 * @return {@link ContractGroups} with the results.
	 */
	public ContractGroups assignContracts(Collection<Advertiser> advertisers,
			Collection<AdNetwork> adNetworks) {
		ContractGroups contractGroups = groupSplitter(advertisers);
		assignBidderContrats(contractGroups.getBidders(), adNetworks);
		assignRankedContrats(contractGroups.getRanked(), adNetworks);
		return contractGroups;
	}

	/**
	 * Assigns {@link MonthlyContract contracts} according to {@link Advertiser
	 * advertisers} result in a performed auction.
	 * 
	 * @param advertisers
	 *            {@link Collection} of {@link Advertiser advertisers} to assign
	 *            {@link MonthlyContract contracts} for.
	 * @param adNetworks
	 *            {@link Collection} of {@link AdNetwork}s to assign
	 *            {@link MonthlyContract contracts} to.
	 * @return {@link Collection} of {@link MonthlyContract monthly contracts}.
	 */
	protected Collection<MonthlyContract> assignBidderContrats(
			Collection<Advertiser> advertisers, Collection<AdNetwork> adNetworks) {
		TreeMap<Integer, MonthlyContract> map = new TreeMap<>();
		Collection<MonthlyContract> monthlyContracts = new LinkedList<>();
		for (Advertiser advertiser : advertisers) {
			monthlyContracts.add(auctionContractAmongAdNetworks(advertiser,
					adNetworks));
		}
		return monthlyContracts;
	}

	/**
	 * Assigns {@link MonthlyContract contracts} according to {@link AdNetwork
	 * ad networks'} ranking.
	 * 
	 * @param advertisers
	 *            {@link Collection} of {@link Advertiser advertisers} to assign
	 *            {@link MonthlyContract contracts} for.
	 * @param adNetworks
	 *            {@link Collection} of {@link AdNetwork}s to assign
	 *            {@link MonthlyContract contracts} to.
	 * @return {@link Collection} of {@link MonthlyContract}s ordered from
	 *         highest to lowest budget.
	 */
	protected Collection<MonthlyContract> assignRankedContrats(
			Collection<Advertiser> advertisers, Collection<AdNetwork> adNetworks) {
		TreeMap<Integer, MonthlyContract> map = new TreeMap<>();
		Collection<MonthlyContract> monthlyContracts = new LinkedList<>();
		for (Advertiser advertiser : advertisers) {
			MonthlyContract monthlyContract = advertiser
					.generateMonthlyContract();
			int monthlyBudget = monthlyContract.getMonthlyBudget();
			while (map.containsKey(monthlyBudget)) {
				monthlyBudget++;
			}
			map.put(monthlyBudget, monthlyContract);
		}
		SortedMap<Double, AdNetwork> adNetworkRanking = getRankingMap(adNetworks);
		while (map.size() > 0) {
			for (AdNetwork adNetwork : adNetworkRanking.values()) {
				if (map.size() == 0) {
					break;
				}
				MonthlyContract monthlyContract = map.remove(map.lastKey());
				(adNetwork).registerMonthlyContract(monthlyContract);
				monthlyContracts.add(monthlyContract);
			}
		}
		return monthlyContracts;
	}

	/**
	 * Generates a {@link SortedMap} of {@link AdNetwork}s according to their
	 * ranking, given by the central {@link RankingAgency}.
	 * 
	 * @param adNetworks
	 *            {@link Collection} of {@link AdNetwork}s to generate the
	 *            {@link SortedMap} for.
	 * @return A {@link SortedMap} of {@link AdNetwork}s according to their
	 *         ranking, given by the central {@link RankingAgency} sorted from
	 *         <b>highest</b> ranking <b>to lowest</b>.
	 */
	protected TreeMap<Double, AdNetwork> getRankingMap(
			Collection<AdNetwork> adNetworks) {
		TreeMap<Double, AdNetwork> sortedMap = new TreeMap<Double, AdNetwork>(
				new Comparator<Double>() {
					@Override
					public int compare(Double a, Double b) {
						return b.compareTo(a);
					}
				});
		for (AdNetwork adNetwork : adNetworks) {
			Double ranking = rankingAgency.getRanking(adNetwork);
			while (sortedMap.containsKey(ranking)) {
				ranking += (1.0 / adNetworks.size());
			}
			sortedMap.put(ranking, adNetwork);
		}
		return sortedMap;
	}

	/**
	 * Splits a {@link Collection} of {@link Advertiser advertisers} into two
	 * groups. <li>bidders - {@link Collection} of {@link Advertiser
	 * advertisers} which will bid for {@link MonthlyContract contracts}</li><li>
	 * ranked - {@link Collection} of {@link Advertiser advertisers} which be
	 * assigned {@link MonthlyContract contracts} by their ranking.</li>
	 * 
	 * @param advertisers
	 *            A {@link Collection} of {@link Advertiser advertisers}.
	 * @return {@link ContractGroups}
	 */
	protected ContractGroups groupSplitter(Collection<Advertiser> advertisers) {
		ContractGroups contractGroups = new ContractGroups();
		for (Advertiser advertiser : advertisers) {
			if (Math.random() > 0.5) {
				contractGroups.getBidders().add(advertiser);
			} else {
				contractGroups.getRanked().add(advertiser);
			}
		}
		return contractGroups;
	}

	private MonthlyContract auctionContractAmongAdNetworks(
			Advertiser advertiser, Collection<AdNetwork> adNetworks) {
		// TODO Auto-generated method stub
		return null;
	}

	/**
	 * Simple pojo to hold data for contract assignment groups.
	 * 
	 * @see ContractAssigner#groupSplitter(Collection)
	 * 
	 * @author greenwald
	 * 
	 */
	@Data
	protected class ContractGroups {
		/**
		 * {@link Collection} of {@link Advertiser advertisers} which will bid
		 * for {@link MonthlyContract contracts}.
		 */
		private Collection<Advertiser> bidders = new HashSet<>();
		/**
		 * {@link Collection} of {@link MonthlyContract contracts} which were
		 * assigned to {@link AdNetwork}s by their ranking.
		 */
		private Collection<Advertiser> ranked = new HashSet<>();
	}

}
